package com.shisan.tingshu.account.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.shisan.tingshu.account.mapper.RechargeInfoMapper;
import com.shisan.tingshu.account.mapper.UserAccountDetailMapper;
import com.shisan.tingshu.account.mapper.UserAccountMapper;
import com.shisan.tingshu.account.service.UserAccountService;
import com.shisan.tingshu.common.execption.ShisanException;
import com.shisan.tingshu.common.result.Result;
import com.shisan.tingshu.common.result.ResultCodeEnum;
import com.shisan.tingshu.model.account.RechargeInfo;
import com.shisan.tingshu.model.account.UserAccount;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.shisan.tingshu.model.account.UserAccountDetail;
import com.shisan.tingshu.vo.account.AccountLockResultVo;
import com.shisan.tingshu.vo.account.AccountLockVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;

@Slf4j
@Service
@SuppressWarnings({"unchecked", "rawtypes"})
public class UserAccountServiceImpl extends ServiceImpl<UserAccountMapper, UserAccount> implements UserAccountService {

	@Autowired
	private UserAccountMapper userAccountMapper;

	@Autowired
	private StringRedisTemplate redisTemplate;

	@Autowired
	private UserAccountDetailMapper userAccountDetailMapper;

	@Autowired
	private RechargeInfoMapper rechargeInfoMapper;

	/**
	 * 查询用户余额
	 * @param userId
	 * @return
	 */
	@Override
	public BigDecimal getAvailableAmount(Long userId) {

		LambdaQueryWrapper<UserAccount> wrapper = new LambdaQueryWrapper<>();
		wrapper.eq(UserAccount::getUserId, userId);
		UserAccount userAccount = userAccountMapper.selectOne(wrapper);
		return userAccount.getAvailableAmount();
	}

	/**
	 * 查询并锁定用户余额
	 * @param accountLockVo
	 * @return
	 */
	// 注意事项一：查询余额是否充足
	// 如果余额不充足，抛出异常
	// 如果余额充足，锁定余额（利用CAS思想，即Compare And Swap比较并交换，在锁定余额之前，查询一下要锁定的余额是否是符合预期的，将查询和锁定余额的操作写在同一条SQL上）
	// 如果不将查询和锁定余额的操作写在同一条SQL上，可能会有以下情况：
	// （1）线程A查询余额是否充足，充足，来锁定余额；
	// （2）在线程A锁定余额之前，线程B查询余额是否充足，充足，来锁定余额；
	// （3）在线程B锁定余额之前，线程A已经将余额锁定，此时线程B锁定的余额就是已经被线程A修改过后的余额，此时就出现了错误。

	// 注意事项二：利用分布式锁解决并发问题
	// 线程A在执行业务的时候，线程B不能进来执行业务。线程A在执行业务之前，加分布式锁，执行业务之后，将锁定金额返回对象保存到Redis中，然后删除分布式锁
	// 线程A执行完业务的时候，线程B才能进来执行业务。线程B进来之后，先查询Redis中是否有锁定金额返回对象，如果有，说明线程A已经执行完业务了，线程B直接将结果返回给用户
	@Override
	@Transactional(rollbackFor = Exception.class)
	public Result<AccountLockResultVo> checkAndLockAmount(AccountLockVo accountLockVo) {

		// 1.获取变量
		Long userId = accountLockVo.getUserId();
		BigDecimal amount = accountLockVo.getAmount();
		String content = accountLockVo.getContent();
		String orderNo = accountLockVo.getOrderNo();
		String dataKey = "user:account:amount:" + orderNo; // 锁定金额返回对象的key
		String lockKey = "user:account:amount:lock" + orderNo; // 余额的分布式锁key
		// 2.加分布式锁
		Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(lockKey, "1");
		if (!aBoolean) {
			return Result.build(null, ResultCodeEnum.ACCOUNT_LOCK_REPEAT); // 线程2进来，线程1已经锁定了，线程2不能进来
		}
		// 3.将判断余额是否充足和修改余额作为原子操作（利用CAS思想，即比较并交换）
		try {
			String result = redisTemplate.opsForValue().get(dataKey);
			if (!StringUtils.isEmpty(result)) { // 如果redis中已经存在锁定金额返回对象，说明已经锁定过余额了，直接将结果返回给线程B
				return Result.build(JSONObject.parseObject(result, AccountLockResultVo.class), ResultCodeEnum.ACCOUNT_LOCK_REPEAT);
			}
			int count = userAccountMapper.checkAndLockAmount(userId, amount);
			// 3.1 锁定失败
			if (count == 0) {
				return Result.build(null, ResultCodeEnum.ACCOUNT_LOCK_ERROR);
			}
			// 3.2 锁定成功，构建锁定金额返回对象，即AccountLockResultVo
			AccountLockResultVo accountLockResultVo = new AccountLockResultVo();
			accountLockResultVo.setUserId(userId);
			accountLockResultVo.setAmount(amount);
			accountLockResultVo.setContent(content);
			// 3.3 记录用户账户流水，即用户对钱有哪些操作
			this.log(userId, amount, "锁定：" + content, orderNo, "1202"); // 流水号 1202 代表锁定金额
			// 3.4 将锁定金额返回对象保存到redis中，主要为了解锁和消费的时候取数据方便
			redisTemplate.opsForValue().set(dataKey, JSONObject.toJSONString(accountLockResultVo));
			// 3.5 返回锁定对象
			return Result.ok(accountLockResultVo);
		} catch (Exception e) {
			// TODO：OpenFeign业务执行期间出现了异常，让OpenFeign进行重试
			// OpenFeign只会对超时和网络异常进行重试，业务异常不进行重试
			throw new ShisanException(400, "服务内部处理数据出现了异常");
		} finally {
			redisTemplate.delete(lockKey);   // 线程1执行完，删除锁，其它线程才可以进来操作余额
		}
	}

	/**
	 * 记录用户账户流水
	 * @param userId
	 * @param amount
	 * @param content
	 * @param orderNo
	 * @param tradeType
	 */
	@Override
	public void log(Long userId, BigDecimal amount, String content, String orderNo, String tradeType) {

		UserAccountDetail userAccountDetail = new UserAccountDetail();
		userAccountDetail.setUserId(userId);
		userAccountDetail.setTitle(content);
		userAccountDetail.setTradeType(tradeType);
		userAccountDetail.setAmount(amount);
		userAccountDetail.setOrderNo(orderNo);
		userAccountDetailMapper.insert(userAccountDetail);
	}

	/**
	 * 根据订单号查询充值信息
	 * @param orderNo
	 * @param userId
	 * @return
	 */
	@Override
	public RechargeInfo getRechargeInfoByOrderNo(String orderNo, Long userId) {

		LambdaQueryWrapper<RechargeInfo> wrapper = new LambdaQueryWrapper<>();
		wrapper.eq(RechargeInfo::getOrderNo, orderNo);
		wrapper.eq(RechargeInfo::getUserId, userId);
		RechargeInfo rechargeInfo = rechargeInfoMapper.selectOne(wrapper);
		return rechargeInfo;
	}

	/**
	 * 查询用户的消费记录
	 * @param page
	 * @param userId
	 * @return
	 */
	@Override
	public IPage<UserAccountDetail> findUserConsumePage(IPage<UserAccountDetail> page, Long userId) {

		return userAccountDetailMapper.findUserConsumePage(page, userId);
	}

	/**
	 * 查询用户的充值记录
	 * @param page
	 * @param userId
	 * @return
	 */
	@Override
	public IPage<UserAccountDetail> findUserRechargePage(IPage<UserAccountDetail> page, Long userId) {

		return userAccountDetailMapper.findUserRechargePage(page, userId);
	}
}
